#region Apache License
//
// Licensed to the Apache Software Foundation (ASF) under one or more 
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership. 
// The ASF licenses this file to you under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with 
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

using System;
using System.Collections;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.IO;
#if NETSTANDARD
using System.Threading.Tasks;
#endif

using log4net.Core;
using log4net.Util;

namespace log4net.Appender
{
  /// <summary>
  /// Appender that allows clients to connect via Telnet to receive log messages
  /// </summary>
  /// <remarks>  
  /// <para>
  /// The TelnetAppender accepts socket connections and streams logging messages
  /// back to the client.  
  /// The output is provided in a telnet-friendly way so that a log can be monitored 
  /// over a TCP/IP socket.
  /// This allows simple remote monitoring of application logging.
  /// </para>
  /// <para>
  /// The default <see cref="Port"/> is 23 (the telnet port).
  /// </para>
  /// </remarks>
  /// <author>Keith Long</author>
  /// <author>Nicko Cadell</author>
  public class TelnetAppender : AppenderSkeleton
  {
    private SocketHandler m_handler;
    private int m_listeningPort = 23;

    #region Constructor

    /// <summary>
    /// Default constructor
    /// </summary>
    /// <remarks>
    /// <para>
    /// Default constructor
    /// </para>
    /// </remarks>
    public TelnetAppender()
    {
    }

    #endregion

    #region Private Static Fields

    /// <summary>
    /// The fully qualified type of the TelnetAppender class.
    /// </summary>
    /// <remarks>
    /// Used by the internal logger to record the Type of the
    /// log message.
    /// </remarks>
    private static readonly Type declaringType = typeof(TelnetAppender);

    #endregion Private Static Fields

    /// <summary>
    /// Gets or sets the TCP port number on which this <see cref="TelnetAppender"/> will listen for connections.
    /// </summary>
    /// <value>
    /// An integer value in the range <see cref="IPEndPoint.MinPort" /> to <see cref="IPEndPoint.MaxPort" /> 
    /// indicating the TCP port number on which this <see cref="TelnetAppender"/> will listen for connections.
    /// </value>
    /// <remarks>
    /// <para>
    /// The default value is 23 (the telnet port).
    /// </para>
    /// </remarks>
    /// <exception cref="ArgumentOutOfRangeException">The value specified is less than <see cref="IPEndPoint.MinPort" /> 
    /// or greater than <see cref="IPEndPoint.MaxPort" />.</exception>
    public int Port
    {
      get
      {
        return m_listeningPort;
      }
      set
      {
        if (value < IPEndPoint.MinPort || value > IPEndPoint.MaxPort)
        {
          throw log4net.Util.SystemInfo.CreateArgumentOutOfRangeException("value", (object)value,
            "The value specified for Port is less than " +
            IPEndPoint.MinPort.ToString(NumberFormatInfo.InvariantInfo) +
            " or greater than " +
            IPEndPoint.MaxPort.ToString(NumberFormatInfo.InvariantInfo) + ".");
        }
        else
        {
          m_listeningPort = value;
        }
      }
    }

    #region Override implementation of AppenderSkeleton

    /// <summary>
    /// Overrides the parent method to close the socket handler
    /// </summary>
    /// <remarks>
    /// <para>
    /// Closes all the outstanding connections.
    /// </para>
    /// </remarks>
    protected override void OnClose()
    {
      base.OnClose();

      if (m_handler != null)
      {
        m_handler.Dispose();
        m_handler = null;
      }
    }

    /// <summary>
    /// This appender requires a <see cref="Layout"/> to be set.
    /// </summary>
    /// <value><c>true</c></value>
    /// <remarks>
    /// <para>
    /// This appender requires a <see cref="Layout"/> to be set.
    /// </para>
    /// </remarks>
    protected override bool RequiresLayout
    {
      get { return true; }
    }

    /// <summary>
    /// Initialize the appender based on the options set.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This is part of the <see cref="IOptionHandler"/> delayed object
    /// activation scheme. The <see cref="ActivateOptions"/> method must 
    /// be called on this object after the configuration properties have
    /// been set. Until <see cref="ActivateOptions"/> is called this
    /// object is in an undefined state and must not be used. 
    /// </para>
    /// <para>
    /// If any of the configuration properties are modified then 
    /// <see cref="ActivateOptions"/> must be called again.
    /// </para>
    /// <para>
    /// Create the socket handler and wait for connections
    /// </para>
    /// </remarks>
    public override void ActivateOptions()
    {
      base.ActivateOptions();
      try
      {
        LogLog.Debug(declaringType, "Creating SocketHandler to listen on port [" + m_listeningPort + "]");
        m_handler = new SocketHandler(m_listeningPort);
      }
      catch (Exception ex)
      {
        LogLog.Error(declaringType, "Failed to create SocketHandler", ex);
        throw;
      }
    }

    /// <summary>
    /// Writes the logging event to each connected client.
    /// </summary>
    /// <param name="loggingEvent">The event to log.</param>
    /// <remarks>
    /// <para>
    /// Writes the logging event to each connected client.
    /// </para>
    /// </remarks>
    protected override void Append(LoggingEvent loggingEvent)
    {
      if (m_handler != null && m_handler.HasConnections)
      {
        m_handler.Send(RenderLoggingEvent(loggingEvent));
      }
    }

    #endregion

    #region SocketHandler helper class

    /// <summary>
    /// Helper class to manage connected clients
    /// </summary>
    /// <remarks>
    /// <para>
    /// The SocketHandler class is used to accept connections from
    /// clients.  It is threaded so that clients can connect/disconnect
    /// asynchronously.
    /// </para>
    /// </remarks>
    protected class SocketHandler : IDisposable
    {
      private const int MAX_CONNECTIONS = 20;

      private Socket m_serverSocket;
      private ArrayList m_clients = new ArrayList();

      /// <summary>
      /// Class that represents a client connected to this handler
      /// </summary>
      /// <remarks>
      /// <para>
      /// Class that represents a client connected to this handler
      /// </para>
      /// </remarks>
      protected class SocketClient : IDisposable
      {
        private Socket m_socket;
        private StreamWriter m_writer;

        /// <summary>
        /// Create this <see cref="SocketClient"/> for the specified <see cref="Socket"/>
        /// </summary>
        /// <param name="socket">the client's socket</param>
        /// <remarks>
        /// <para>
        /// Opens a stream writer on the socket.
        /// </para>
        /// </remarks>
        public SocketClient(Socket socket)
        {
          m_socket = socket;

          try
          {
            m_writer = new StreamWriter(new NetworkStream(socket));
          }
          catch
          {
            Dispose();
            throw;
          }
        }

        /// <summary>
        /// Write a string to the client
        /// </summary>
        /// <param name="message">string to send</param>
        /// <remarks>
        /// <para>
        /// Write a string to the client
        /// </para>
        /// </remarks>
        public void Send(String message)
        {
          m_writer.Write(message);
          m_writer.Flush();
        }

        #region IDisposable Members

        /// <summary>
        /// Cleanup the clients connection
        /// </summary>
        /// <remarks>
        /// <para>
        /// Close the socket connection.
        /// </para>
        /// </remarks>
        public void Dispose()
        {
          try
          {
            if (m_writer != null)
            {
              m_writer.Dispose();
              m_writer = null;
            }
          }
          catch { }

          if (m_socket != null)
          {
            try
            {
              m_socket.Shutdown(SocketShutdown.Both);
            }
            catch { }

            try
            {
#if NET_4_0 || NETSTANDARD
              m_socket.Dispose();
#else
              m_socket.Close();
#endif
            }
            catch { }

            m_socket = null;
          }
        }

        #endregion
      }

      /// <summary>
      /// Opens a new server port on <paramref ref="port"/>
      /// </summary>
      /// <param name="port">the local port to listen on for connections</param>
      /// <remarks>
      /// <para>
      /// Creates a socket handler on the specified local server port.
      /// </para>
      /// </remarks>
      public SocketHandler(int port)
      {
        m_serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        m_serverSocket.Bind(new IPEndPoint(IPAddress.Any, port));
        m_serverSocket.Listen(5);
        AcceptConnection();
      }

      private void AcceptConnection()
      {
#if NETSTANDARD
        m_serverSocket.AcceptAsync().ContinueWith(OnConnect, TaskScheduler.Default);
#else
        m_serverSocket.BeginAccept(new AsyncCallback(OnConnect), null);
#endif
      }

      /// <summary>
      /// Sends a string message to each of the connected clients
      /// </summary>
      /// <param name="message">the text to send</param>
      /// <remarks>
      /// <para>
      /// Sends a string message to each of the connected clients
      /// </para>
      /// </remarks>
      public void Send(String message)
      {
        ArrayList localClients = m_clients;

        foreach (SocketClient client in localClients)
        {
          try
          {
            client.Send(message);
          }
          catch (Exception)
          {
            // The client has closed the connection, remove it from our list
            client.Dispose();
            RemoveClient(client);
          }
        }
      }

      /// <summary>
      /// Add a client to the internal clients list
      /// </summary>
      /// <param name="client">client to add</param>
      private void AddClient(SocketClient client)
      {
        lock (this)
        {
          ArrayList clientsCopy = (ArrayList)m_clients.Clone();
          clientsCopy.Add(client);
          m_clients = clientsCopy;
        }
      }

      /// <summary>
      /// Remove a client from the internal clients list
      /// </summary>
      /// <param name="client">client to remove</param>
      private void RemoveClient(SocketClient client)
      {
        lock (this)
        {
          ArrayList clientsCopy = (ArrayList)m_clients.Clone();
          clientsCopy.Remove(client);
          m_clients = clientsCopy;
        }
      }

      /// <summary>
      /// Test if this handler has active connections
      /// </summary>
      /// <value>
      /// <c>true</c> if this handler has active connections
      /// </value>
      /// <remarks>
      /// <para>
      /// This property will be <c>true</c> while this handler has
      /// active connections, that is at least one connection that 
      /// the handler will attempt to send a message to.
      /// </para>
      /// </remarks>
      public bool HasConnections
      {
        get
        {
          ArrayList localClients = m_clients;

          return (localClients != null && localClients.Count > 0);
        }
      }


#if NETSTANDARD
      private void OnConnect(Task<Socket> acceptTask)
#else
      /// <summary>
      /// Callback used to accept a connection on the server socket
      /// </summary>
      /// <param name="asyncResult">The result of the asynchronous operation</param>
      /// <remarks>
      /// <para>
      /// On connection adds to the list of connections 
      /// if there are two many open connections you will be disconnected
      /// </para>
      /// </remarks>
      private void OnConnect(IAsyncResult asyncResult)
#endif
      {
        try
        {
#if NETSTANDARD
          Socket socket = acceptTask.GetAwaiter().GetResult();
#else
          // Block until a client connects
          Socket socket = m_serverSocket.EndAccept(asyncResult);
#endif
          LogLog.Debug(declaringType, "Accepting connection from [" + socket.RemoteEndPoint.ToString() + "]");
          SocketClient client = new SocketClient(socket);

          int currentActiveConnectionsCount = m_clients.Count;
          if (currentActiveConnectionsCount < MAX_CONNECTIONS)
          {
            try
            {
              client.Send("TelnetAppender v1.0 (" + (currentActiveConnectionsCount + 1) + " active connections)\r\n\r\n");
              AddClient(client);
            }
            catch
            {
              client.Dispose();
            }
          }
          else
          {
            client.Send("Sorry - Too many connections.\r\n");
            client.Dispose();
          }
        }
        catch
        {
        }
        finally
        {
          if (m_serverSocket != null)
          {
            AcceptConnection();
          }
        }
      }

      #region IDisposable Members

      /// <summary>
      /// Close all network connections
      /// </summary>
      /// <remarks>
      /// <para>
      /// Make sure we close all network connections
      /// </para>
      /// </remarks>
      public void Dispose()
      {
        ArrayList localClients = m_clients;

        foreach (SocketClient client in localClients)
        {
          client.Dispose();
        }
        m_clients.Clear();

        Socket localSocket = m_serverSocket;
        m_serverSocket = null;
        try
        {
          localSocket.Shutdown(SocketShutdown.Both);
        }
        catch
        {
        }

        try
        {
#if NET_2_0
          localSocket.Close();
#else
          localSocket.Dispose();
#endif
        }
        catch
        {
        }
      }

      #endregion
    }

    #endregion
  }
}
